// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using ILLink.Shared;
using Mono.Cecil;

namespace Mono.Linker
{
    public class MemberActionStore
    {
        public SubstitutionInfo PrimarySubstitutionInfo { get; }
        private readonly Dictionary<AssemblyDefinition, SubstitutionInfo?> _embeddedXmlInfos;
        private readonly Dictionary<MethodDefinition, bool> _featureCheckValues;
        readonly LinkContext _context;

        public MemberActionStore(LinkContext context)
        {
            PrimarySubstitutionInfo = new SubstitutionInfo();
            _embeddedXmlInfos = new Dictionary<AssemblyDefinition, SubstitutionInfo?>();
            _featureCheckValues = new Dictionary<MethodDefinition, bool>();
            _context = context;
        }

        private bool TryGetSubstitutionInfo(MemberReference member, [NotNullWhen(true)] out SubstitutionInfo? xmlInfo)
        {
            var assembly = member.Module.Assembly;
            if (!_embeddedXmlInfos.TryGetValue(assembly, out xmlInfo))
            {
                xmlInfo = _context.EmbeddedXmlInfo.ProcessSubstitutions(assembly, _context);
                _embeddedXmlInfos.Add(assembly, xmlInfo);
            }

            return xmlInfo != null;
        }

        public MethodAction GetAction(MethodDefinition method)
        {
            if (PrimarySubstitutionInfo.MethodActions.TryGetValue(method, out MethodAction action))
                return action;

            if (TryGetSubstitutionInfo(method, out var embeddedXml))
            {
                if (embeddedXml.MethodActions.TryGetValue(method, out action))
                    return action;
            }

            if (TryGetFeatureCheckValue(method, out _))
                return MethodAction.ConvertToStub;

            return MethodAction.Nothing;
        }

        public bool TryGetMethodStubValue(MethodDefinition method, out object? value)
        {
            if (PrimarySubstitutionInfo.MethodStubValues.TryGetValue(method, out value))
                return true;

            if (TryGetSubstitutionInfo(method, out var embeddedXml)
                && embeddedXml.MethodStubValues.TryGetValue(method, out value))
                return true;

            if (TryGetFeatureCheckValue(method, out bool bValue))
            {
                value = bValue ? 1 : 0;
                return true;
            }

            return false;
        }

        internal bool TryGetFeatureCheckValue(MethodDefinition method, out bool value)
        {
            if (_featureCheckValues.TryGetValue(method, out value))
                return true;

            value = false;

            if (!method.IsStatic)
                return false;

            if (method.ReturnType.MetadataType != MetadataType.Boolean)
                return false;

            if (FindProperty(method) is not PropertyDefinition property)
                return false;

            if (property.SetMethod != null)
                return false;

            foreach (var featureSwitchDefinitionAttribute in _context.CustomAttributes.GetCustomAttributes(property, "System.Diagnostics.CodeAnalysis", "FeatureSwitchDefinitionAttribute"))
            {
                if (featureSwitchDefinitionAttribute.ConstructorArguments is not [CustomAttributeArgument { Value: string switchName }])
                    continue;

                // If there's a FeatureSwitchDefinition, don't continue looking for FeatureGuard.
                // We don't want to infer feature switch settings from FeatureGuard.
                if (_context.FeatureSettings.TryGetValue(switchName, out value))
                {
                    _featureCheckValues[method] = value;
                    return true;
                }
                return false;
            }

            if (!_context.IsOptimizationEnabled(CodeOptimizations.SubstituteFeatureGuards, method))
                return false;

            foreach (var featureGuardAttribute in _context.CustomAttributes.GetCustomAttributes(property, "System.Diagnostics.CodeAnalysis", "FeatureGuardAttribute"))
            {
                if (featureGuardAttribute.ConstructorArguments is not [CustomAttributeArgument { Value: TypeReference featureType }])
                    continue;

                if (featureType.Namespace == "System.Diagnostics.CodeAnalysis")
                {
                    switch (featureType.Name)
                    {
                        case "RequiresUnreferencedCodeAttribute":
                            _featureCheckValues[method] = value;
                            return true;
                        case "RequiresDynamicCodeAttribute":
                            if (_context.FeatureSettings.TryGetValue(
                                    "System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported",
                                    out bool isDynamicCodeSupported)
                                && !isDynamicCodeSupported)
                            {
                                _featureCheckValues[method] = value;
                                return true;
                            }
                            break;
                    }
                }
            }

            return false;

            static PropertyDefinition? FindProperty(MethodDefinition method)
            {
                if (!method.IsGetter)
                    return null;

                foreach (var property in method.DeclaringType.Properties)
                {
                    if (property.GetMethod == method)
                        return property;
                }

                return null;
            }
        }

        public bool TryGetFieldUserValue(FieldDefinition field, out object? value)
        {
            if (PrimarySubstitutionInfo.FieldValues.TryGetValue(field, out value))
                return true;

            if (!TryGetSubstitutionInfo(field, out var embeddedXml))
                return false;

            return embeddedXml.FieldValues.TryGetValue(field, out value);
        }
    }
}
